/**
 * Pyx4me framework
 * Copyright (C) 2006-2008 pyx4j.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
 * @author vlads
 * @version $Id$
 */
package com.github.wvengen.maven.proguard;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.maven.archiver.MavenArchiveConfiguration;
import org.apache.maven.archiver.MavenArchiver;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.apache.tools.ant.DefaultLogger;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Java;
import org.codehaus.plexus.archiver.jar.JarArchiver;

/**
 *
 * <p>
 * The Obfuscate task provides a stand-alone obfuscation task
 * </p>
 *
 * @goal proguard
 * @phase package
 * @description Create small jar files using ProGuard
 * @requiresDependencyResolution compile
 * @threadSafe
 */

public class ProGuardMojo extends AbstractMojo {

    /**
     * Set this to 'true' to bypass ProGuard processing entirely.
     *
     * @parameter property="proguard.skip"
     */
    private boolean                     skip;

    /**
     * Recursively reads configuration options from the given file filename
     *
     * @parameter default-value="${basedir}/proguard.conf"
     */
    private File                        proguardInclude;

    /**
     * Select specific ProGuard version from plugin dependencies
     *
     * @parameter
     */
    private String                      proguardVersion;

    /**
     * ProGuard configuration options
     *
     * @parameter
     */
    private String[]                    options;

    /**
     * Specifies not to obfuscate the input class files.
     *
     * @parameter default-value="true"
     */
    private boolean                     obfuscate;

    /**
     * Specifies that project compile dependencies be added as -libraryjars to proguard arguments. Dependency itself is
     * not included in resulting jar unless you set includeDependencyInjar to true
     * 
     * @parameter default-value="true"
     */
    private boolean                     includeDependency;

    /**
     * Specifies that project compile dependencies should be added as injar.
     * 
     * @parameter default-value="false"
     */
    private boolean                     includeDependencyInjar;

    /**
     * Bundle project dependency to resulting jar. Specifies list of artifact inclusions
     *
     * @parameter
     */
    private Assembly                    assembly;

    /**
     * Additional -libraryjars e.g. ${java.home}/lib/rt.jar Project compile dependency are added automatically. See
     * exclusions
     *
     * @parameter
     */
    private List<String>                libs;

    /**
     * List of dependency exclusions
     *
     * @parameter
     */
    private List<String>                exclusions;

    /**
     * Specifies the input jar name (or wars, ears, zips) of the application to be
     * processed.
     *
     * You may specify a classes directory e.g. 'classes'. This way plugin will processed
     * the classes instead of jar. You would need to bind the execution to phase 'compile'
     * or 'process-classes' in this case.
     *
     * @parameter expression="${project.build.finalName}.jar"
     * @required
     */
    protected String                    injar;

    /**
     * Set this to 'true' to bypass ProGuard processing when injar does not exists.
     *
     * @parameter default-value="false"
     */
    private boolean                     injarNotExistsSkip;

    /**
     * Apply ProGuard classpathentry Filters to input jar. e.g. <code>!**.gif,!**&#47;tests&#47;**'</code>
     *
     * @parameter
     */
    protected String                    inFilter;

    /**
     * Specifies the names of the output jars. If attach=true the value ignored and name constructed base on classifier
     * If empty input jar would be overdriven.
     *
     * @parameter
     */
    protected String                    outjar;

    /**
     * Apply ProGuard classpathentry Filters to output jar. e.g. <code>!**.gif,!**&#47;tests&#47;**'</code>
     *
     * @parameter
     */
    protected String                    outFilter;

    /**
     * Specifies whether or not to attach the created artifact to the project
     *
     * @parameter default-value="false"
     */
    private boolean                     attach            = false;

    /**
     * Specifies attach artifact type
     *
     * @parameter default-value="jar"
     */
    private String                      attachArtifactType;

    /**
     * Specifies attach artifact Classifier, Ignored if attach=false
     *
     * @parameter default-value="small"
     */
    private String                      attachArtifactClassifier;

    /**
     * Set to false to exclude the attachArtifactClassifier from the Artifact final name. Default value is true.
     *
     * @parameter default-value="true"
     */
    private boolean                     appendClassifier;

    /**
     * Set to true to include META-INF/maven/** maven descriptord
     *
     * @parameter default-value="false"
     */
    private boolean                     addMavenDescriptor;

    /**
     * Directory containing the input and generated JAR.
     *
     * @parameter property="project.build.directory"
     * @required
     */
    protected File                      outputDirectory;

    /**
     * The Maven project reference where the plugin is currently being executed. The default value is populated from
     * maven.
     *
     * @parameter property="project"
     * @readonly
     * @required
     */
    protected MavenProject              mavenProject;

    /**
     * The plugin dependencies.
     *
     * @parameter property="plugin.artifacts"
     * @required
     * @readonly
     */
    protected List<String>              pluginArtifacts;

    /**
     * @component
     */
    private MavenProjectHelper          projectHelper;

    /**
     * The Jar archiver.
     *
     * @component role="org.codehaus.plexus.archiver.Archiver" roleHint="jar"
     * @required
     */
    private JarArchiver                 jarArchiver;

    /**
     * The maven archive configuration to use. only if assembly is used.
     *
     * @parameter
     */
    protected MavenArchiveConfiguration archive           = new MavenArchiveConfiguration();

    /**
     * The max memory the forked java process should use, e.g. 256m
     *
     * @parameter
     */
    protected String                    maxMemory;

    /**
     * ProGuard main class name.
     *
     * @parameter default-value="proguard.ProGuard"
     */
    protected String                    proguardMainClass = "proguard.ProGuard";

    /**
     * Sets the name of the ProGuard mapping file.
     *
     * @parameter default-value="proguard_map.txt"
     */
    protected String                    mappingFileName   = "proguard_map.txt";

    /**
     * Sets the name of the ProGuard seed file.
     *
     * @parameter default-value="proguard_seed.txt"
     */
    protected String                    seedFileName      = "proguard_seeds.txt";

    private Log                         log;

    /**
     * ProGuard docs: Names with special characters like spaces and parentheses must be quoted with single or double
     * quotes.
     */
    private static String fileNameToString(String fileName) {
        return "'" + fileName + "'";
    }

    private static String fileToString(File file) {
        return fileNameToString(file.toString());
    }

    private boolean useArtifactClassifier() {
        return appendClassifier
               && ((attachArtifactClassifier != null) && (attachArtifactClassifier.length() > 0));
    }

    public void execute() throws MojoExecutionException, MojoFailureException {
        defaultExcute();

    }

    private void defaultExcute() throws MojoFailureException, MojoExecutionException {
        log = getLog();

        if (skip) {
            log.info("Bypass ProGuard processing because \"proguard.skip=true\"");
            return;
        }

        boolean mainIsJar = mavenProject.getPackaging().equals("jar");

        File inJarFile = new File(outputDirectory, injar);
        if (!inJarFile.exists()) {
            if (injarNotExistsSkip) {
                log.info("Bypass ProGuard processing because \"injar\" dos not exist");
                return;
            } else if (mainIsJar) {
                throw new MojoFailureException("Can't find file " + inJarFile);
            }
        }

        if (!outputDirectory.exists()) {
            if (!outputDirectory.mkdirs()) {
                throw new MojoFailureException("Can't create " + outputDirectory);
            }
        }

        File outJarFile;
        boolean sameArtifact;

        if (attach) {
            outjar = nameNoType(injar);
            if (useArtifactClassifier()) {
                outjar += "-" + attachArtifactClassifier;
            }
            outjar += "." + attachArtifactType;
        }

        if ((outjar != null) && (!outjar.equals(injar))) {
            sameArtifact = false;
            outJarFile = (new File(outputDirectory, outjar)).getAbsoluteFile();
            if (outJarFile.exists()) {
                if (!deleteFileOrDirectory(outJarFile)) {
                    throw new MojoFailureException("Can't delete " + outJarFile);
                }
            }
        } else {
            sameArtifact = true;
            outJarFile = inJarFile.getAbsoluteFile();
            File baseFile;
            if (inJarFile.isDirectory()) {
                baseFile = new File(outputDirectory, nameNoType(injar) + "_proguard_base");
            } else {
                baseFile = new File(outputDirectory, nameNoType(injar) + "_proguard_base.jar");
            }
            if (baseFile.exists()) {
                if (!deleteFileOrDirectory(baseFile)) {
                    throw new MojoFailureException("Can't delete " + baseFile);
                }
            }
            if (inJarFile.exists()) {
                if (!inJarFile.renameTo(baseFile)) {
                    throw new MojoFailureException("Can't rename " + inJarFile);
                }
            }
            inJarFile = baseFile;
        }

        ArrayList<String> args = new ArrayList<String>();

        if (log.isDebugEnabled()) {
            @SuppressWarnings("unchecked")
            List<Artifact> dependancy = mavenProject.getCompileArtifacts();
            for (Iterator<Artifact> i = dependancy.iterator(); i.hasNext();) {
                Artifact artifact = i.next();
                log.debug("--- compile artifact " + artifact.getGroupId() + ":"
                          + artifact.getArtifactId() + ":" + artifact.getType() + ":"
                          + artifact.getClassifier() + " Scope:" + artifact.getScope());
            }
            for (Iterator i = mavenProject.getArtifacts().iterator(); i.hasNext();) {
                Artifact artifact = (Artifact) i.next();
                log.debug("--- artifact " + artifact.getGroupId() + ":" + artifact.getArtifactId()
                          + ":" + artifact.getType() + ":" + artifact.getClassifier() + " Scope:"
                          + artifact.getScope());
            }
            for (Iterator i = mavenProject.getDependencies().iterator(); i.hasNext();) {
                Dependency artifact = (Dependency) i.next();
                log.debug("--- dependency " + artifact.getGroupId() + ":"
                          + artifact.getArtifactId() + ":" + artifact.getType() + ":"
                          + artifact.getClassifier() + " Scope:" + artifact.getScope());
            }
        }

        Set<String> inPath = new HashSet<String>();
        boolean hasInclusionLibrary = false;
        if (assembly != null) {
            for (Iterator iter = assembly.inclusions.iterator(); iter.hasNext();) {
                Inclusion inc = (Inclusion) iter.next();
                if (!inc.library) {
                    File file = getClasspathElement(getDependancy(inc, mavenProject), mavenProject);
                    inPath.add(file.toString());
                    log.debug("--- ADD injars:" + inc.artifactId);
                    StringBuffer filter = new StringBuffer(fileToString(file));
                    filter.append("(!META-INF/MANIFEST.MF");
                    if (!addMavenDescriptor) {
                        filter.append(",");
                        filter.append("!META-INF/maven/**");
                    }
                    if (inc.filter != null) {
                        filter.append(",").append(inc.filter);
                    }
                    filter.append(")");
                    args.add("-injars");
                    args.add(filter.toString());
                } else {
                    hasInclusionLibrary = true;
                    log.debug("--- ADD libraryjars:" + inc.artifactId);
                    // This may not be CompileArtifacts, maven 2.0.6 bug
                    File file = getClasspathElement(getDependancy(inc, mavenProject), mavenProject);
                    inPath.add(file.toString());
                    args.add("-libraryjars");
                    args.add(fileToString(file));
                }
            }
        }

        if (inJarFile.exists()) {
            args.add("-injars");
            StringBuffer filter = new StringBuffer(fileToString(inJarFile));
            if ((inFilter != null) || (!addMavenDescriptor)) {
                filter.append("(");
                boolean coma = false;

                if (!addMavenDescriptor) {
                    coma = true;
                    filter.append("!META-INF/maven/**");
                }

                if (inFilter != null) {
                    if (coma) {
                        filter.append(",");
                    }
                    filter.append(inFilter);
                }

                filter.append(")");
            }
            args.add(filter.toString());
        }

        if (includeDependency) {
            List dependency = this.mavenProject.getCompileArtifacts();
            for (Iterator i = dependency.iterator(); i.hasNext();) {
                Artifact artifact = (Artifact) i.next();
                // dependency filter
                if (isExclusion(artifact)) {
                    continue;
                }
                File file = getClasspathElement(artifact, mavenProject);

                if (inPath.contains(file.toString())) {
                    log.debug("--- ignore library since one in injar:" + artifact.getArtifactId());
                    continue;
                }
                if (includeDependencyInjar) {
                    log.debug("--- ADD library as injars:" + artifact.getArtifactId());
                    args.add("-injars");
                } else {
                    log.debug("--- ADD libraryjars:" + artifact.getArtifactId());
                    args.add("-libraryjars");

                }
                args.add(fileToString(file));
            }
        }

        if (args.contains("-injars")) {
            args.add("-outjars");
            StringBuffer filter = new StringBuffer(fileToString(outJarFile));
            if (outFilter != null) {
                filter.append("(").append(outFilter).append(")");
            }
            args.add(filter.toString());
        }

        if (!obfuscate) {
            args.add("-dontobfuscate");
        }

        if (proguardInclude != null) {
            if (proguardInclude.exists()) {
                args.add("-include");
                args.add(fileToString(proguardInclude));
                log.debug("proguardInclude " + proguardInclude);
            } else {
                log.debug("proguardInclude config does not exists " + proguardInclude);
            }
        }

        if (libs != null) {
            for (Iterator i = libs.iterator(); i.hasNext();) {
                Object lib = i.next();
                args.add("-libraryjars");
                args.add(fileNameToString(lib.toString()));
            }
        }

        args.add("-printmapping");
        args.add(fileToString((new File(outputDirectory, mappingFileName).getAbsoluteFile())));

        args.add("-printseeds");
        args.add(fileToString((new File(outputDirectory, seedFileName).getAbsoluteFile())));

        if (log.isDebugEnabled()) {
            args.add("-verbose");
        }

        if (options != null) {
            for (int i = 0; i < options.length; i++) {
                args.add(options[i]);
            }
        }

        log.info("execute ProGuard " + args.toString());
        File proguardJar = getProguardJar(this);
        proguardMain(proguardJar, args, this);

        if ((assembly != null) && (hasInclusionLibrary)) {

            log.info("creating assembly");

            File baseFile = new File(outputDirectory, nameNoType(injar) + "_proguard_result.jar");
            if (baseFile.exists()) {
                if (!baseFile.delete()) {
                    throw new MojoFailureException("Can't delete " + baseFile);
                }
            }
            File archiverFile = outJarFile.getAbsoluteFile();
            if (!outJarFile.renameTo(baseFile)) {
                throw new MojoFailureException("Can't rename " + outJarFile);
            }

            MavenArchiver archiver = new MavenArchiver();

            archiver.setArchiver(jarArchiver);
            archiver.setOutputFile(archiverFile);
            archive.setAddMavenDescriptor(addMavenDescriptor);

            try {
                jarArchiver.addArchivedFileSet(baseFile);

                for (Iterator iter = assembly.inclusions.iterator(); iter.hasNext();) {
                    Inclusion inc = (Inclusion) iter.next();
                    if (inc.library) {
                        File file;
                        Artifact artifact = getDependancy(inc, mavenProject);
                        file = getClasspathElement(artifact, mavenProject);
                        if (file.isDirectory()) {
                            getLog()
                                .info("merge project: " + artifact.getArtifactId() + " " + file);
                            jarArchiver.addDirectory(file);
                        } else {
                            getLog().info("merge artifact: " + artifact.getArtifactId());
                            jarArchiver.addArchivedFileSet(file);
                        }
                    }
                }

                archiver.createArchive(mavenProject, archive);

            } catch (Exception e) {
                throw new MojoExecutionException("Unable to create jar", e);
            }

        }

        if (attach && !sameArtifact) {
            //操作war解压，等一系列操作
            String absolutePath = outJarFile.getAbsolutePath();
            getLog().info("---absolutePath--" + absolutePath);
            String unzipPath = outJarFile.getParent() + "/proguard-war";
            getLog().info("删除路径" + absolutePath);
            deleteDir(unzipPath);
            File unZipFile = new File(unzipPath);
            unZipFile.mkdir();
            String targetWar = mavenProject.getBuild().getDirectory() + "/"
                               + mavenProject.getBuild().getFinalName() + ".war";
            getLog().info(targetWar);
            WarUtils.unzip(targetWar, unzipPath);
            //删除路径"
            deleteDir(unzipPath + "/WEB-INF/classes");
            WarUtils.unzip(absolutePath, unzipPath + "/WEB-INF/classes");
            outJarFile.delete();
            WarUtils.zip(absolutePath, outJarFile.getParent() + "/proguard-war");
            getLog().info("---absolutePath--" + absolutePath);
            if (useArtifactClassifier()) {
                projectHelper.attachArtifact(mavenProject, attachArtifactType,
                    attachArtifactClassifier, outJarFile);
            } else {
                projectHelper.attachArtifact(mavenProject, attachArtifactType, null, outJarFile);
            }
        }
    }

    /**
     * 递归删除目录下的所有文件及子目录下所有文件
     * @param dir 将要删除的文件目录
     * @return boolean Returns "true" if all deletions were successful.
     *                 If a deletion fails, the method stops attempting to
     *                 delete and returns "false".
     */
    private static boolean deleteDir(String path) {
        File dir = new File(path);
        if (dir.isDirectory()) {
            String[] children = dir.list();
            //递归删除目录中的子目录下
            for (int i = 0; i < children.length; i++) {
                boolean success = deleteDir(dir + "/" + children[i]);
                if (!success) {
                    return false;
                }
            }
        }
        // 目录此时为空，可以删除
        return dir.delete();
    }

    private static File getProguardJar(ProGuardMojo mojo) throws MojoExecutionException {

        Artifact proguardArtifact = null;
        int proguardArtifactDistance = -1;
        // This should be solved in Maven 2.1
        for (Iterator i = mojo.pluginArtifacts.iterator(); i.hasNext();) {
            Artifact artifact = (Artifact) i.next();
            mojo.getLog().debug("pluginArtifact: " + artifact.getFile());
            if (artifact.getArtifactId().startsWith("proguard")
                && !artifact.getArtifactId().startsWith("proguard-maven-plugin")) {
                int distance = artifact.getDependencyTrail().size();
                mojo.getLog().debug("proguard DependencyTrail: " + distance);
                if ((mojo.proguardVersion != null)
                    && (mojo.proguardVersion.equals(artifact.getVersion()))) {
                    proguardArtifact = artifact;
                    break;
                } else if (proguardArtifactDistance == -1) {
                    proguardArtifact = artifact;
                    proguardArtifactDistance = distance;
                } else if (distance < proguardArtifactDistance) {
                    proguardArtifact = artifact;
                    proguardArtifactDistance = distance;
                }
            }
        }
        if (proguardArtifact != null) {
            mojo.getLog().debug("proguardArtifact: " + proguardArtifact.getFile());
            return proguardArtifact.getFile().getAbsoluteFile();
        }
        mojo.getLog().info("proguard jar not found in pluginArtifacts");

        ClassLoader cl;
        cl = mojo.getClass().getClassLoader();
        // cl = Thread.currentThread().getContextClassLoader();
        String classResource = "/" + mojo.proguardMainClass.replace('.', '/') + ".class";
        URL url = cl.getResource(classResource);
        if (url == null) {
            throw new MojoExecutionException("Obfuscation failed ProGuard ("
                                             + mojo.proguardMainClass + ") not found in classpath");
        }
        String proguardJar = url.toExternalForm();
        if (proguardJar.startsWith("jar:file:")) {
            proguardJar = proguardJar.substring("jar:file:".length());
            proguardJar = proguardJar.substring(0, proguardJar.indexOf('!'));
        } else {
            throw new MojoExecutionException("Unrecognized location (" + proguardJar
                                             + ") in classpath");
        }
        return new File(proguardJar);
    }

    private static void proguardMain(File proguardJar, ArrayList argsList, ProGuardMojo mojo)
                                                                                             throws MojoExecutionException {

        Java java = new Java();

        Project antProject = new Project();
        antProject.setName(mojo.mavenProject.getName());
        antProject.init();

        DefaultLogger antLogger = new DefaultLogger();
        antLogger.setOutputPrintStream(System.out);
        antLogger.setErrorPrintStream(System.err);
        antLogger.setMessageOutputLevel(mojo.log.isDebugEnabled() ? Project.MSG_DEBUG
            : Project.MSG_INFO);

        antProject.addBuildListener(antLogger);
        antProject.setBaseDir(mojo.mavenProject.getBasedir());

        java.setProject(antProject);
        java.setTaskName("proguard");

        mojo.getLog().info("proguard jar: " + proguardJar);

        java.createClasspath().setLocation(proguardJar);
        // java.createClasspath().setPath(System.getProperty("java.class.path"));
        java.setClassname(mojo.proguardMainClass);

        java.setFailonerror(true);

        java.setFork(true);

        // get the maxMemory setting
        if (mojo.maxMemory != null) {
            java.setMaxmemory(mojo.maxMemory);
        }

        for (Iterator i = argsList.iterator(); i.hasNext();) {
            java.createArg().setValue(i.next().toString());
        }

        int result = java.executeJava();
        if (result != 0) {
            throw new MojoExecutionException("Obfuscation failed (result=" + result + ")");
        }
    }

    private static String nameNoType(String fileName) {
        int extStart = fileName.lastIndexOf('.');
        if (extStart == -1) {
            return fileName;
        }
        return fileName.substring(0, extStart);
    }

    private static boolean deleteFileOrDirectory(File path) throws MojoFailureException {
        if (path.isDirectory()) {
            File[] files = path.listFiles();
            for (int i = 0; i < files.length; i++) {
                if (files[i].isDirectory()) {
                    if (!deleteFileOrDirectory(files[i])) {
                        throw new MojoFailureException("Can't delete dir " + files[i]);
                    }
                } else {
                    if (!files[i].delete()) {
                        throw new MojoFailureException("Can't delete file " + files[i]);
                    }
                }
            }
            return path.delete();
        } else {
            return path.delete();
        }
    }

    private static Artifact getDependancy(Inclusion inc, MavenProject mavenProject)
                                                                                   throws MojoExecutionException {
        Set dependancy = mavenProject.getArtifacts();
        for (Iterator i = dependancy.iterator(); i.hasNext();) {
            Artifact artifact = (Artifact) i.next();
            if (inc.match(artifact)) {
                return artifact;
            }
        }
        throw new MojoExecutionException("artifactId Not found " + inc.artifactId);
    }

    private boolean isExclusion(Artifact artifact) {
        if (exclusions == null) {
            return false;
        }
        for (Iterator iter = exclusions.iterator(); iter.hasNext();) {
            Exclusion excl = (Exclusion) iter.next();
            if (excl.match(artifact)) {
                return true;
            }
        }
        return false;
    }

    private static File getClasspathElement(Artifact artifact, MavenProject mavenProject)
                                                                                         throws MojoExecutionException {
        if (artifact.getClassifier() != null) {
            return artifact.getFile();
        }
        String refId = artifact.getGroupId() + ":" + artifact.getArtifactId();
        MavenProject project = (MavenProject) mavenProject.getProjectReferences().get(refId);
        if (project != null) {
            return new File(project.getBuild().getOutputDirectory());
        } else {
            File file = artifact.getFile();
            if ((file == null) || (!file.exists())) {
                throw new MojoExecutionException("Dependency Resolution Required " + artifact);
            }
            return file;
        }
    }
}
